package org.orm.hsc;

import javax.sql.DataSource;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


/**
 * @author
 * @date 2025-04-09
 * @Description jdbc执行器，封装了jdbc功能    <br/>
 * jdbc的主要功能是CRUD= 增删改 + 查 <br/>
 */
public class JdbcExecutor {

    /**
     * 使用一次executor，就相当于执行了一次jdbc
     * 一次jdbc的多次sql执行 只需要一个连接即可
     */
    private Connection conn;

    /**
     * 控制是否自动提交事务
     */
    private boolean isAutoCommit;

    /**
     * 数据库连接池 <br/>
     * 预留接口
     */
    private DataSource pool;

    /**
     * 数据库连接信息放在配置文件中
     */
    private Configuration configuration;

    //有这个方法后，就可以加载动态的驱动信息了
    //但一个程序中，多次都是与同一个数据库交互，只需要加载一次就可以了。
    //需要额外的控制
    private boolean isDriver = false;

    //存储自增主键值
    private List<Long> generatedKeys;

    public Long[] getGeneratedKeys() {
        // 转为数组
        return generatedKeys.toArray(new Long[]{});
    }

    /**
     * 获取自增主键值 绝大多数的insert操作，都是单条的。
     *
     * @return
     */
    public Long getGeneratedKey() {
        return generatedKeys.get(0);
    }

    public void setGeneratedKeys(List<Long> generatedKeys) {
        this.generatedKeys = generatedKeys;
    }

    /**
     * 无参构造器默认设置事务手动提交
     */
    public JdbcExecutor() {
        this(false);
    }

    // 构造方法重载
    public JdbcExecutor(boolean isAutoCommit) {
        // 设置事务提交方式
        this.isAutoCommit = isAutoCommit;
        //未来我们可能会使用数据库连接池获得连接
        //  数据库连接比较占资源，创建连接本身也是一个比较耗时的过程
        //  在没有任何管理的情况下，每次jdbc都需要创建一个连接，每次jdbc结束后，都会关闭回收连接。
        //  性能低，资源利用率不高。
        //  可以通过连接池提前创建连接，当我们使用连接时，连接早就创建完了，只需要从连接池获得就可以了
        //  使用完毕后，可以将连接归还给连接池，下次在进行分配复用。
        conn = getConnection();
        // 进行事务的控制
        try {
            conn.setAutoCommit(isAutoCommit);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //-----------------加入configuration-------------------------
    // 构造方法重载
    //  创建连接
    //  使用一次executor，就相当于执行了一次jdbc
    //  一次jdbc就只能执行一次sql ？ 不是。可以执行多个sql
    //  一次jdbc的多次sql执行，需要几个连接？   1个  （节省资源，可以事务处理）
    //  未来使用executor，通过doUpdate执行一次增删改 再通过doQuery执行一次查询，都应该属于1个连接
    //  可以在普通代码段或构造器中执行创建连接的操作
    public JdbcExecutor(Configuration configuration, boolean isAutoCommit) {
        this.configuration = configuration;
        this.isAutoCommit = isAutoCommit;
        //动态(信息)加载驱动
        if (!isDriver) {
            this.loadDriver();
        }
        // 初始化conn
        conn = getConnection();
        try {
            conn.setAutoCommit(isAutoCommit);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 获取连接的方法
     * 这里预留为以后从数据库连接池中进行获取
     *
     * @return
     */
    private Connection getConnection() {
        //判断，如果存在连接池，就从连接池中获得连接。 如果没有连接池，就创建连接
        //预留连接池 从连接池中获得连接
        try {
            if (pool != null) {
                // 从数据库获取连接
                return pool.getConnection();
            } else {
                // 数据库的连接信息放在Configuration 使用硬编码方式  也就是说直接把配置新放在底代码上 来设置数据库的连接信息
//                    return  conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false", "root", "Admin@1234");
                // 使用配置对象方式
                return conn = DriverManager.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 从配置文件中读取 加载驱动信息
     */
    private void loadDriver() {
        try {
            Class.forName(configuration.getDriver());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }


    }


    /**
     * 为删除，修改和不需要自增主键值的insert来使用的
     * 如果调用该方法的话 默认不返回自增主键值
     *
     * @param sql
     * @param params
     */
    public void doUpdate(String sql, Object... params) {
        this.doUpdate(sql, false, params);
    }


    /**
     * jdbc执行器，封装了jdbc功能    <br/>
     * jdbc的主要功能是CRUD= 增删改  并且可以返回多个主键值
     */
    public int doUpdate(String sql, boolean isGeneratedKeys, Object... params) {
        //1 引入驱动jar文件 (非编码实现)
        //2 加载驱动
        //  未来每一次jdbc操作都会使用executor，对于多次的jdbc操作，加载驱动只需要做一次
        //  可以在静态代码段中执行

        //3 创建连接
        //  使用一次executor，就相当于执行了一次jdbc
        //  一次jdbc就只能执行一次sql ？ 不是。可以执行多个sql
        //  一次jdbc的多次sql执行，需要几个连接？   1个  （节省资源，可以事务处理）
        //  未来使用executor，通过doUpdate执行一次增删改 再通过doQuery执行一次查询，都应该属于1个连接
        //  可以在普通代码段或构造器中执行创建连接的操作

        //4 创建命令集对象
        //  问题1：创建命令集时需要预处理sql ， 这个sql是不确定的，未来不同的使用者使用executor执行操作时，应该会有不同的sql
        //        所以这个sql应该以参数的形式传递进来
        //  问题2：预处理sql时，可能有?。预处理结束后，需要为?传递对应的参数值
        //        这个参数值是哪里来的呢？ 应该也是使用者传递的.
        //        因为参数的类型和个数不确定，所以可以使用可变参数 Object...params
        //           对外，使用者可以不传参，传1个参，传多个参
        //           对内，无论使用者传递多少个参数，都是数组  length=0 , length=1 , length=n

        PreparedStatement preparedStatement = null;
        //需要获得自增主键值
        try {
            if (isGeneratedKeys) {
                preparedStatement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            } else {
                preparedStatement = conn.prepareStatement(sql);
            }
            //4.5 为预处理sql中的?传递参数
            //    params[]
            for (int i = 0; i < params.length; i++) {
                preparedStatement.setObject(i + 1, params[i]);
            }

            //5 执行sql
            int count = preparedStatement.executeUpdate();
            //如果需要返回主键值
            if (isGeneratedKeys) {
                // 6 获得自增主键值
                ResultSet resultSet = preparedStatement.getGeneratedKeys();
                generatedKeys = new ArrayList<>();
                while (resultSet.next()) {
                    long cno = resultSet.getLong(1);
                    System.out.println("本次自增值为：" + cno);
                    generatedKeys.add(cno);
                }
            }
            //6 各种关闭
            //  stmt + conn
            //  无论此次jdbc执行成功与否，都应该将资源释放。应该在finally中释放
            //      随着未来对连接池的使用，这个释放就有两种含义 （关闭 ， 归还）
            return count;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(preparedStatement);
        }
    }

    /**
     * 处理新规则的sql 比如： insert into car(cname,color,price) values(#{cname},#{color},#{price})
     *
     * @param sql
     * @param isGeneratedKeys
     * @param paramObj
     * @return
     */
    public int doUpdate(String sql, boolean isGeneratedKeys, Object paramObj) {
        //将新规则的sql 处理成原始sql，并完成相关的其他处理 （整理sql中？对应的参数名）（从paramObj中获得对应的参数值）
        //再调用之前的doUpdate完成操作

        try {
            SqlHandler sqlHandler = new SqlHandler();
            // 1、处理sql 语句 将 insert into car(cname,color,price) values(#{cname},#{color},#{price}) 转化为对应的 ? ---》 insert into car(cname,color,price) values(?,?,?)
            // 2、该方法的第二个功能 是保存sql 中的每一个 key  #{key}--> [cname,color,price]
            sqlHandler.executeSql(sql);
            // 获取对象的属性值 并且保存在数组中 paramValues
            sqlHandler.executeParam(paramObj);
            // sql 进行执行  insert into car(cname,color,price) values(?,?,?) 并且对参数进行赋值
            return this.doUpdate(sqlHandler.getOldSql(), isGeneratedKeys, sqlHandler.getParamValues());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }


    /**
     * 关闭 Statement 对象抽取
     *
     * @param stmt
     */
    private void close(Statement stmt) {
        try {
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 结果集对象和Statement对象方法关闭抽取
     *
     * @param resultSet
     * @param stmt
     */
    private void close(ResultSet resultSet, Statement stmt) {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 关闭连接对象抽取
     */
    public void close() {
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public void commit() {
        try {

            if (!isAutoCommit && conn != null) {
                conn.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 泛型的解析：
     * 1、第一个<T> 表示返回的类型是不确定的 暂时使用T 来表示
     * 2、List<T> 表示返回的结果集的转为对象时 对象的类型是不确定个 跟 <T> 类型保持一致
     * 3、 Class<T> 传入的类型 也是不确定的个 使用 Class<T> 来表示
     * 4、只要 Class<T> 传入的参数类型 T 确定了 List<T> 的类型也确定了
     *
     * @param sql
     * @param rowType
     * @param params
     * @param <T>     指定类型 通过泛型指定
     * @return
     */
    public <T> List<T> doQuery(String sql, Class<T> rowType, Object... params) {
        //1 引入驱动jar （非编码）
        //2 加载驱动 （static代码块中进行加载了） 驱动只需要一次
        //3 获得连接 （constructor） new JdbcExecutor() 的时候 也进行创建连接了 一个JdbcExecutor 使用一个连接即可
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //4 创建命令集
            preparedStatement = conn.prepareStatement(sql);
            //4.5 为sql中的?匹配参数
            setPreparedParams(preparedStatement, params);
            //5 执行sql
            resultSet = preparedStatement.executeQuery();
            //5.5 将结果集中的数据 处理转换成 java所需要的类型数据 通过ResultHandler 对sql返回的结果集转为Java对应的类型  rs --> List<?>
            ResultHandler resultHandler = new ResultHandler();
            // 处理结果集映射到Java对应的类型
            List<T> list = resultHandler.handler(resultSet, rowType);

            //6 各种关闭 rs + stmt  （finally）
            return list;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            close(resultSet, preparedStatement);
        }
    }

    private void setPreparedParams(PreparedStatement preparedStatement,  Object[] params) throws SQLException {
        System.out.println("Params: " + Arrays.toString(params) + ", Length: " + (params != null ? params.length : "null"));

        if (params != null && params.length > 0 && params[0] != null){
            // 预处理对象设置参数
            for (int i = 0; i < params.length; i++) {
                preparedStatement.setObject(i + 1, params[i]);
            }

        }

    }

    /**
     * 提供手动控制回滚的方法
     */
    public void rollback() {
        try {
            if (conn != null && !isAutoCommit) {

                conn.rollback();
            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //---------------动态sql新增API----------------
    public int executeUpdate(String sqlid, boolean isGeneratedKey, Object paramObj) {
        // 获取SqlHandler
        try {
            SqlHandler handler = configuration.getHandler(sqlid);
            // 将参数设置到SqlHandler中
            handler.executeParam(paramObj);
            return this.doUpdate(handler.getOldSql(), isGeneratedKey, handler.getParamValues());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
